3.5. 循环神经网络(RNN)实战:LSTM自动生成文本
本文承接自上一篇文章 3.3. 循环神经网络(RNN)理论(进阶),没看过的建议先看上文。
3.5.1. 准备工作
请在终端输入指令以下载和安装:
pip install keras tensorflow scikit-learn
这里需要使用到我的txt文件,我把它上传到了GitCode,你点击链接即可查看和下载。我们使用Mary Shelley的Frankenstein(中文名《弗兰肯斯坦》《科学怪人》)作为训练数据。
把下载好后的数据放到你的Python项目下即可。
3.5.2. 模型架构
在这篇文章中我们会使用长短期记忆网络(LSTM)模型来进行文本生成。我们要把文本内容转化之后的字典喂给模型,然后让它基于前20个字符预测第21个字符,计算准确率。
每个输入样本是长度为 20 的时间步(字符)序列;LSTM 层有 20 个单元,输出层神经元数取决于文本中不同字符的个数。
3.5.3. 敲代码
Step 1: 加载数据
首先我们把数据加载进来,顺便去掉换行符:
# 加载数据
data = open('Frankenstein_by_Mary_Shelley.txt').read()
# 移除换行符
data = data.replace('\n', ' ').replace('\r', '')
Step 2: 建立字典
文本中每一个字符我们都需要在字典中建立一个字段来对应,首先我们需要去掉重复的字符,然后再建立字典:
# 字符去重
letters = list(set(data))
# 打印检查
print(len(letters))
# 建立字典
char_to_int = {b:a for a, b in enumerate(letters)}
-
通过
set(data)将文本数据中的所有字符去重,保留每种字符的唯一值(如字母、数字、标点符号等)。list(set(data))就是将这些唯一的字符变成一个列表,存储在变量letters中。 -
字典
char_to_int的键是相应的字符,值是从序号 0 开始的整数,这样可以通过字符转为数据再转为one-hot格式。
输出:
74
这74个不同的字符(大小写字母、数字、标点等)就组成了所有的文本内容。
Step 3: 输入处理
我们接下来需要对字符串进行批量处理,在这里我们先建立一个data_processing方法,之后我们可以通过调用它来提取模型能够接收到的X和y数据:
# 字符批量处理
import numpy as np
from tensorflow.keras.utils import to_categorical
#实现输入字符文章的批量处理,输入整个字符、滑动窗口大小、转化字典
def data_processing(data, slide, num_letters, char_to_int):
# 滑动窗门提取数据
def extract_data(data, slide):
x = []
y = []
for i in range(len(data) - slide):
x.append([a for a in data[i:i + slide]])
y.append(data[i + slide])
return x, y
# 字符到数字的批量处理
def char_to_int_data(x, y, char_to_int):
x_to_int = []
y_to_int = []
for i in range(len(x)):
x_to_int.append([char_to_int[char] for char in x[i]])
y_to_int.append([char_to_int[char] for char in y[i]])
return x_to_int, y_to_int
char_data = extract_data(data, slide)
int_data = char_to_int_data(char_data[0], char_data[1], char_to_int)
Input = int_data[0]
Output = list(np.array(int_data[1]).flatten())
Input_Reshaped = np.array(Input).reshape(len(Input), slide)
new_ = np.zeros((Input_Reshaped.shape[0], Input_Reshaped.shape[1], num_letters))
for i in range(Input_Reshaped.shape[0]):
for j in range(Input_Reshaped.shape[1]):
new_[i, j, :] = to_categorical(Input_Reshaped[i, j], num_letters)
return new_, Output
extract_data 函数:
- 作用:通过滑动窗口,从输入字符序列中提取样本。
x:窗口中的字符列表(长度为窗口大小)。y:窗口后紧跟的目标字符(预测值)。
- 逻辑:
x通过[a for a in data[i:i + slide]]获取当前窗口范围内的字符。y取窗口后第一个字符data[i + slide]。- 如:
slide=3,输入数据data="abcdefg",结果:x = [['a', 'b', 'c'], ['b', 'c', 'd'], ['c', 'd', 'e'], ['d', 'e', 'f']]y = ['d', 'e', 'f', 'g']
char_to_int_data 函数:
- 作用:将字符序列
x和目标字符y转换成整数序列。- 使用
char_to_int字典获取每个字符在字典中的整数索引。 - 如:输入
x = [['a', 'b', 'c'], ['b', 'c', 'd']],y = ['d', 'e'],字典{'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4},结果:x_to_int = [[0, 1, 2], [1, 2, 3]]y_to_int = [[3], [4]](之后在Output中会被展平为[3, 4])。
- 使用
转换为模型输入:
- 通过
extract_data和char_to_int_data提取并完成字符到整数的转换; - 原始的整数输入
Input的形状为[样本数, 窗口大小]; Output是目标字符序列,以一维数组形式存储。
one-hot编码处理:
- 作用:将整数输入
Input_Reshaped转换为独热编码的 3D 输入new_。Input_Reshaped[i, j]是窗口的第i个样本中第j个字符的整数值。to_categorical(Input_Reshaped[i, j], num_letters)构建字符的独热编码,维度为[num_letters]。- 每个样本的编码结果存储在
new_,其形状为[样本数, 窗口大小, num_letters]。
最终输出:
new_:one-hot编码的输入数组,形状[样本数, 窗口大小, 字符表大小],适用于LSTM这类深度学习模型。Output:目标序列的整数数组,一维形式,长度为样本数。
Step4: 获取模型的输入
我们接下来使用data_processing方法提取模型能够接收到的X和y数据,同时也确认一下时间步长(time step)为20(模型基于前20个字符预测):
#时间步长
time_step = 20
# 获取模型的输入数据
X, y = data_processing(data, time_step, len(letters), char_to_int)
# 检查数据结构
print(X.shape)
print(len(y))
输出:
(421397, 20, 74)
421397
-
(421397, 20, 74):421397:表示滑动窗口提取到的样本数量,即构造出的有多少个[输入序列 -> 目标字符]样本。这取决于输入文本长度len(data)和滑动窗口大小time_step的关系。20:表示每个样本的时间步数 (滑动窗口大小time_step),也就是每个输入序列包含 20 个字符。这个值由代码中的参数time_step = 20决定。74:表示字符总种类数,即one-hot编码的维度。这个值来源于字符表的大小len(letters),表示输入数据中有 74 个独一无二的字符(包括字母、标点符号、空格等)。
-
y是样本对应的目标字符的整数列表。每个样本有一个目标字符,因此y的长度与样本数量相同,也就是421397。
Step5: 数据分离
把数据分为训练集和测试集:
# 数据分离
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=10)
# 把y_train改为one-hot编码
y_train_category = to_categorical(y_train, num_classes=len(letters))
- 将
y_train转换为one-hot 编码的主要原因是为了能交给LSTM模型来训练
Step 6: 建立模型
模型接收长度为 20 的时间步序列;LSTM 有 20 个单元,输出层神经元数取决于文本中不同字符的个数,也就是字典的长度:
# 建立模型
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM
model = Sequential()
model.add(LSTM(units=20, input_shape=(time_step, len(letters)), activation='relu'))
model.add(Dense(len(letters), activation='softmax'))
Step 7: 配置模型
# 配置模型
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
Step 8: 训练模型
把训练集数据喂给模型即可:
# 训练模型
model.fit(X_train, y_train_category, epochs=20, batch_size=1000)
batch_size=1000:
- 每次训练(梯度更新)所使用的样本数量。
- 数据会被分割为多个批次,每批包含
1000个样本。- 举例:如果训练样本总数为 50,000,每个批次是 1,000,则一个 epoch 内需要 50 次更新。
- 当然如果你内存多可以不用设置这个参数,一次性把所有数据写入内存(要占恐怖的15GB左右,还没有算IDE)
Step 9: 评估模型
我们通过准确率来评估模型:
# 训练集准确率
_, train_accuracy = model.evaluate(X_train, y_train_category)
print(f"Train Accuracy: {train_accuracy}")
# 测试集准确率
y_test_category = to_categorical(y_test, num_classes=len(letters))
_, test_accuracy = model.evaluate(X_test, y_test_category)
print(f"Test Accuracy: {test_accuracy}")
输出:
Train Accuracy: 0.4766451120376587
Test Accuracy: 0.4727812111377716
别看这个准确率还没到50%,但是要知道计算机需要从74个不同字符中猜出一个答案能到这个正确率已经非常不错了。盲猜的正确率只有1/74,也就是1.35%,而模型运算出的结果正确率比它高30多倍。